上一篇文章我們深入瞭解了 Operation 的 transform
function 是如何實作針對各種不同的 Operation types 的更新功能的,並略過了一切關於 Location type 的更新相關的內容。包含 selection
的主要更新邏輯也一樣,我們只提到了它們是交由各自對應到的 transform
method api 來處理,它們分別是:
Path.transform
Point.transform
Range.transform
我們在 Day 18 時就有先剪短提到過這幾個 methods 的功用了,它們同時負責 Location value 的更新以及確保存在於編輯器裡的 Locations 都是 Immutable 的。
基本上整個編輯器就是仰賴前一篇介紹的 transform
function 以及今天的這三個 transform
method apis 作為資料更新最底層的功能,就像是蓋房子一樣,其他如 Operations 或是最 high-level 的 Transform methods 都只是基於它們再向上搭建另外一層功能而已
前一篇是介紹 Node types 的更新,相信今天的任務是什麼就不用多贅述了 ?
Path.transform
這個 method 主要需要傳入兩個參數,分別是 path
、 operation
。
path
:主要傳入接受更新的路徑資料,會因應傳入的 operation 回傳相對應更新過後的 Immutable value 。operation
:執行的 Operation 資料,下方的內容會很常取出在 operation.path
alias 為 op
使用,之後在這個小節看到 op
代表的就是 operation.path
的 value 。還有一個 options.affinity
,我們在後續遇到它的使用情境時再介紹它的功用
/**
* Transform a path by an operation.
*/
transform(
path: Path,
operation: Operation,
options: { affinity?: 'forward' | 'backward' | null } = {}
): Path | null {
return produce(path, p => {
// ... Path.transform implementation
});
}
insert_node
會受到『插入節點』所影響的路徑就只有三種:
做的操作一樣都是將 path 陣列裡與 op index 同層的值 + 1
case 'insert_node': {
const { path: op } = operation
if (
Path.equals(op, p) ||
Path.endsBefore(op, p) ||
Path.isAncestor(op, p)
) {
p[op.length - 1] += 1
}
break
}
就稍嫌 tricky 一點了,注意到它指定 + 1 的陣列 index 是 op
的 index 值,所以如果今天插入的節點路徑是位於 path
上方的祖先時,我們會將與 op
index 同層的值 + 1 ,因為是它祖先那一層被異動到
remove_node
將與『被刪除的節點』相同,或位於它子層的路徑直接刪除。
case 'remove_node': {
const { path: op } = operation
if (Path.equals(op, p) || Path.isAncestor(op, p)) {
return null
}
// ... else if statement
break
}
如果被刪除的節點路徑位於 path
之前的 sibling 的話則將 index 的值 - 1
else if (Path.endsBefore(op, p)) {
p[op.length - 1] -= 1
}
merge_node
『合併節點』可以分為兩種情形:
op 與 path 相等,或為 path 之前的 sibling :
直接將 path 對應的 index 值 - 1
case 'merge_node': {
const { path: op, position } = operation
if (Path.equals(op, p) || Path.endsBefore(op, p)) {
p[op.length - 1] -= 1
}
// ... else if statement
break
}
op 為 path 的祖先路徑:
除了將對應的 index 值 -1 之外,我們還需要子層的 index 值去加上 position
,也就是它前一個 sibling 的 index 值
else if (Path.isAncestor(op, p)) {
p[op.length - 1] -= 1
p[op.length] += position
}
split_node
『拆分節點』可以分為三種情形:
op
與 path
相等
這裡就會受到 options.affinity
所影響了, affinity
在這裡代表的是將原始的 path
指向的節點向後( forward )或是向前( backward )做拆分,如果是向後拆分則將原始路徑的 index + 1 ,向前則不需要做任何操作。
case 'split_node': {
const { path: op, position } = operation
if (Path.equals(op, p)) {
if (affinity === 'forward') {
p[p.length - 1] += 1
} else if (affinity === 'backward') {
// Nothing, because it still refers to the right path.
} else {
return null
}
}
// ... other statements
break
}
op
與 path
同層,並位於 path
前面的 sibling
直接將 path 的 index 值 + 1 。
else if (Path.endsBefore(op, p)) {
p[op.length - 1] += 1
}
op
為 path
的祖先,且 op
index 的子層對應到的 path
index 位於 position
的後方:
同層的 index 值 + 1 ,同時子層節點扣除掉拆分的 position offset
else if (Path.isAncestor(op, p) && path[op.length] >= position) {
p[op.length - 1] += 1
p[op.length] -= position
}
move_node
首先擋掉了 no-op ,也就是不用執行任何操作,新舊路徑皆相等的情形
case 'move_node': {
const { path: op, newPath: onp } = operation
// If the old and new path are the same, it's a no-op.
if (Path.equals(op, onp)) {
return
}
// ... If else implementations
break
}
接著拆分成四種情形去探討:
op
( Operation 裡欲移動的舊路徑 )為 p
的祖先或 op
與 p
的路徑相等:
此時的 p
被包含在 Operation 的搬遷範圍內,我們要將 onp
( Operation 裡欲移動到的新路徑 )補上 op
index 之後、 p
所涵蓋到的子層。
if (Path.isAncestor(op, p) || Path.equals(op, p)) {
const copy = onp.slice()
// ... if statement for the edge case
return copy.concat(p.slice(op.length))
}
上圖的結果就是:[0, 2]
會向前調動到 [0, 1]
,而 [0, 1]
的整個 branch 則會一起向後移動一個 sibling 。
再來這個情形還有一個 edge case 是需要注意的,也就是當 op
位於 onp
之前,同時為 onp
的上層路徑。
此時我們會需要調整 onp
於 op
層的 index value 將它 -1 ,因為 op
會遭到移除
if (Path.endsBefore(op, onp) && op.length < onp.length) {
copy[op.length - 1] -= 1
}
op
與 onp
同層,且 onp
為 p
的祖先或 onp
與 p
的路徑相等:
此時我們需要調整 p
於 op
index 層的 value 。如果 op 位於 p 之前,代表移動後 value 會因為該層前面的 sibling 遭到拔除因而需要 -1 ;反之則代表該層會有新的 sibling 被移動到前面因而需要 +1
else if (
Path.isSibling(op, onp) &&
(Path.isAncestor(onp, p) || Path.equals(onp, p))
) {
if (Path.endsBefore(op, p)) {
p[op.length - 1] -= 1
} else {
p[op.length - 1] += 1
}
}
onp
位於 p
之前,或 onp
與 p
相等,或 onp
為 p
之祖先:
代表我們要將某一組路徑搬遷到 p
之前,因此我們需要將 p
於 onp
index 層的 value +1
else if (
Path.endsBefore(onp, p) ||
Path.equals(onp, p) ||
Path.isAncestor(onp, p)
) {
// ... if statement for the edge case
p[onp.length - 1] += 1
}
需要注意的 edge case 為:如果要搬遷的 op
位於 p
之前,代表會有一組路徑從 p
之前的位置受到搬遷,我們需要扣除 p
於 op
層的 index value
if (Path.endsBefore(op, p)) {
p[op.length - 1] -= 1
}
op
位於 p
之前:
代表我們要將某一組路徑從 p 之前搬遷走,因此我們需要將 p
於 op
index 層的 value -1
else if (Path.endsBefore(op, p)) {
if (Path.equals(onp, p)) {
p[onp.length - 1] += 1
}
p[op.length - 1] -= 1
}
Path.equals(onp, p)
應該已經在前一組 else if 中被篩掉了才對。歡迎在下方留言補充!Point.transform
主要需要傳入兩個參數,分別是 point
、 op
。
point
:主要傳入接受更新的 Point 資料,會因應傳入的 operation 回傳相對應更新過後的 Immutable value 。op
:執行的 Operation 資料 。還有一個 options.affinity
,我們一樣在後續遇到它的使用情境時再介紹它的功用
/**
* Transform a point by an operation.
*/
transform(
point: Point,
op: Operation,
options: { affinity?: 'forward' | 'backward' | null } = {}
): Point | null {
return produce(point, p => {
const { affinity = 'forward' } = options
const { path, offset } = p
// ... Point.transform implementation
});
}
insert_node
& move_node
因為不會異動到 offset
的值所以實作上非常的 simple ,就只是呼叫 Path.transform 把參數都丟進去而已。
case 'insert_node':
case 'move_node': {
p.path = Path.transform(path, op, options)!
break
}
insert_text
如果 path
與 op.path
為同一路徑,且 op.offset
小於等於 offset
,則代表插入的文字位於欲更新的 Point 之前,因此將 p.offset
增加 op.text
的長度。
case 'insert_text': {
if (Path.equals(op.path, path) && o <= offset) {
p.offset += op.text.length
}
break
}
merge_node
當 op.path
與 path
為同一個路徑時,將 position
加進 offset
裡,再將 path
丟入 Path.transform
case 'merge_node': {
if (Path.equals(op.path, path)) {
p.offset += op.position
}
p.path = Path.transform(path, op, options)!
break
}
remove_text
要移除的文字 op.offset
位於 offset
之前,代表刪除的文字位於欲更新的 Point 之前,因此扣除掉兩個 offset 之間的長度,最長扣除到 op.text
的長度
case 'remove_text': {
if (Path.equals(op.path, path) && op.offset <= offset) {
p.offset -= Math.min(offset - op.offset, op.text.length)
}
break
}
remove_node
如果 op.path
與 path
為相同路徑,或為它的祖先,則直接設為 null
,否則則丟入 Path.transform
case 'remove_text': {
if (Path.equals(op.path, path) && op.offset <= offset) {
p.offset -= Math.min(offset - op.offset, op.text.length)
}
break
}
split_node
我們能先區分成兩種情形: op.path
與 path
是否為相同的路徑,如果不同則不會異動到 offset
value ,直接將 path
丟入 Path.transform
就好
case 'split_node': {
if (Path.equals(op.path, path)) {
// Implementation
} else {
p.path = Path.transform(path, op, options)!
}
break
}
}
這裡的 options.affinity 用途與 Path 段落介紹的 split_node
裡的一樣,代表著節點向後( Forward )或向前( Backward )做拆分。
如果是預設的向後拆分則將在其前面被拆走的 offset
扣掉並一樣丟入 Path.transform
if (op.position === offset && affinity == null) {
return null
} else if (
op.position < offset ||
(op.position === offset && affinity === 'forward')
) {
p.offset -= op.position
p.path = Path.transform(path, op, {
...options,
affinity: 'forward',
})!
}
affinity: null
在這邊做的事情跟 Path.transform
的 null
affinity 一樣,將與 Operation 操作相同的 Path 或 Point 節點直接移除,回傳 null
。但筆者不太確定它的使用情境就是了,一樣歡迎下方留言補充!Range.transform
這裡頭主要都是針對 affinity
參數的控制,最後將 range 的 anchor
與 focus
point 丟入 Point.transform
進行轉換並回傳新的 Immutable Range :
/**
* Transform a range by an operation.
*/
transform(
range: Range,
op: Operation,
options: {
affinity?: 'forward' | 'backward' | 'outward' | 'inward' | null
} = {}
): Range | null {
// ... affinity statement control
return produce(range, r => {
const anchor = Point.transform(r.anchor, op, { affinity: affinityAnchor })
const focus = Point.transform(r.focus, op, { affinity: affinityFocus })
if (!anchor || !focus) {
return null
}
r.anchor = anchor
r.focus = focus
})
},
這裡的 affinity
除了 'forward'
與 'backward'
與前面在 Path 與 Point 提到的用法一樣,是決定節點的拆分( split_node
)方向之外,還有 'inward'
(向內)以及 'outward'
(向外)額外兩種情形。中間的判斷式就是拿來決定 Range 裡的 anchor
與 focus
point 要分別以哪種 affinity
進行 transform 的
const { affinity = 'inward' } = options
let affinityAnchor: 'forward' | 'backward' | null
let affinityFocus: 'forward' | 'backward' | null
if (affinity === 'inward') {
if (Range.isForward(range)) {
affinityAnchor = 'forward'
affinityFocus = 'backward'
} else {
affinityAnchor = 'backward'
affinityFocus = 'forward'
}
} else if (affinity === 'outward') {
if (Range.isForward(range)) {
affinityAnchor = 'backward'
affinityFocus = 'forward'
} else {
affinityAnchor = 'forward'
affinityFocus = 'backward'
}
} else {
affinityAnchor = affinity
affinityFocus = affinity
}
Operation 章節到這邊總算告一個段落了,這個章節的後兩篇我們非常深入地去探討了 Operation 底層的程式碼是如何運作的,希望這能讓讀者在以 slate 為基礎開發編輯器時能更清楚在每一個操作的背後,編輯器實際上都是如何更新的。
下一篇開始我們要來探討 slate 是如何做骯髒標記與完成資料正規化的。
Normalization 在 slate 中也是一個非常重要的功能,它也支援開發者加入自定義的 Normalizing constraints 。
除了探討其中的運作方式,我們也會介紹有哪些原始存在的 constraints 、他們的存在意義以及如何自定義 constaints 。
明天新的篇章見囉~